Skip to content

Add kind discriminator for MCP resource server tools and resources#3550

Open
sahandilshan wants to merge 1 commit into
thunder-id:mainfrom
sahandilshan:mcp-rs-kind-backend
Open

Add kind discriminator for MCP resource server tools and resources#3550
sahandilshan wants to merge 1 commit into
thunder-id:mainfrom
sahandilshan:mcp-rs-kind-backend

Conversation

@sahandilshan

@sahandilshan sahandilshan commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Purpose

Model MCP tools and resources as first-class primitives in resource servers so the product (and the Console) can speak MCP's language, per #3465. An MCP tool and an MCP resource are both actions distinguished by a kind (tool | resource); a Thunder resource acts as an optional grouping namespace. Thunder stores only what authorization needs — the permission plus the kind discriminator — while the MCP server remains the owner of per-primitive definitions (URI, input schema, content). API- and CUSTOM-type resource servers are unchanged.

Approach

  • kind is stored in ACTION.PROPERTIES JSONB and surfaced as a flat field, mirroring the existing RESOURCE_SERVER.delimiter pattern (read path resolveActionProperties, write path buildActionPropertiesJSON; written as NULL when empty so non-MCP actions are byte-for-byte unchanged). The ActionKind type and Action.Kind field live in pkg/thunderidengine/providers alongside the domain types.
  • kind is optional on create for all resource server types:
    • MCP — an omitted kind defaults to tool; a provided kind must be tool|resource.
    • API / CUSTOMkind may be set if desired but is not required; omitted means no kind (unchanged behavior).
    • A provided-but-invalid kind400 for any type.
  • kind is immutable after create — the update path does not accept it; the stored value is preserved.
  • For MCP servers, cross-entity handle/permission collisions (a grouping resource and a tool/resource deriving the same permission string) are rejected on both the REST and declarative-import paths.
  • REST: kind on the create request/response; GET /resource-servers/{id}/actions?kind=tool|resource filter (Postgres PROPERTIES->>'kind' / SQLite json_extract, with a matching filtered count). Declarative import/export preserve kind (MCP import also applies the default). api/resource.yaml documents the field, the filter, and behavior.
  • Authorization is unchanged: kind never participates in permission derivation or exact-string RBAC matching.

Related Issues

Related PRs

Checklist

  • Followed the contribution guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
    • Ran Vale and fixed all errors and warnings
  • Tests provided.
    • Unit Tests
    • Integration Tests
  • Breaking changes. (Fill if applicable)
    • Breaking changes section filled.
    • breaking change label added.

Security checks

  • Followed secure coding standards in WSO2 Secure Coding Guidelines
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets.

Summary by CodeRabbit

  • New Features

    • Added support for filtering action lists by kind (tool or resource) across the app.
    • Action details now include kind information, and new actions can carry a kind value when created.
  • Bug Fixes

    • Improved validation for invalid kind values.
    • Enforced consistent kind behavior when updating actions.
    • Added stricter checks for handle length and MCP-specific requirements.
  • Documentation

    • Updated API descriptions and examples to cover filtered and unfiltered action list results.

@sahandilshan sahandilshan requested a review from senthalan as a code owner June 25, 2026 18:11
@sahandilshan

Copy link
Copy Markdown
Contributor Author

Console UI counterpart: #3551

@sahandilshan sahandilshan added the breaking change The feature/ improvement will alter the existing behaviour label Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds ActionKind handling across OpenAPI contracts, HTTP request/response models, resource service validation, storage persistence and filtering, and declarative resource export/import. Action lists can now be filtered by kind, and MCP action and resource validation rules now enforce kind and handle constraints.

Changes

MCP ActionKind Rollout

Layer / File(s) Summary
Public contract and type definitions
api/resource.yaml, backend/pkg/thunderidengine/providers/model.go, backend/pkg/thunderidengine/providers/model_test.go, backend/internal/resource/model.go
OpenAPI, provider types, and HTTP DTOs add ActionKind, kind-aware request/response fields, and the updated handle length.
HTTP handlers and caller wiring
backend/internal/resource/handler.go, backend/internal/resource/handler_test.go, backend/internal/authzen/service.go, backend/internal/authzen/service_test.go
Action-list and create handlers parse and forward kind, and authzen callers and tests pass the new GetActionList argument.
Service validation and action lifecycle
backend/internal/resource/service.go, backend/internal/resource/ResourceServiceInterface_mock_test.go, backend/internal/resource/service_test.go
Resource service validates MCP handle requirements, action kind rules, handle collisions, kind-filtered listing, and kind immutability on update; the interface mock and tests follow the new signature.
Core store persistence and queries
backend/internal/resource/store.go, backend/internal/resource/store_constants.go, backend/internal/resource/resourceStoreInterface_mock_test.go, backend/internal/resource/store_test.go
The core store persists action kind in ACTION.PROPERTIES, adds kind-filtered queries, and updates the store interface mock and round-trip tests.
Store adapters and merge logic
backend/internal/resource/file_based_store.go, backend/internal/resource/composite_store.go, backend/internal/resource/file_based_store_test.go, backend/internal/resource/composite_store_test.go
File-based and composite stores thread kind through action list/count calls, filter and merge kind-specific results, and update their tests for the new signatures.
Declarative export, parse, and MCP checks
backend/internal/resource/declarative_resource.go, backend/internal/resource/declarative_resource_test.go
Declarative export adds handle, type, and action kind, parsing enforces MCP kind rules, and MCP duplicate-permission checks are added with matching tests.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • thunder-id/thunderid#1402: Updates the same GetActionList call chain and interface signatures, so the action-list plumbing changes overlap.
  • thunder-id/thunderid#3179: Touches the authzen action-search flow that now forwards the extra kind argument into resource action listing.
  • thunder-id/thunderid#3187: Adds resource-server type plumbing that the MCP-specific action kind rules now depend on.

Suggested reviewers

  • rajithacharith
  • darshanasbg
  • senthalan
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding an MCP action kind discriminator for tools and resources.
Description check ✅ Passed The description follows the template with Purpose, Approach, Related Issues, Related PRs, Checklist, and Security checks filled in.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

Affected Assets, Files, and Routes:

view changes for bundle: console-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/dist-*.js 136.41kB 136.56kB 88577.27% ⚠️
assets/dist-*.js -75 bytes 70 bytes -51.72%
assets/dist-*.js -18 bytes 130 bytes -12.16%
assets/dist-*.js 24 bytes 154 bytes 18.46% ⚠️
assets/dist-*.js 75 bytes 145 bytes 107.14% ⚠️
assets/dist-*.js -136.41kB 148 bytes -99.89%

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/internal/resource/declarative_resource_test.go (1)

154-187: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the default server fixture kind-free.

This server has no MCP Type, so it defaults to CUSTOM; asserting ActionKindTool here codifies a state that parseToResourceServer rejects for non-MCP servers. Drop Kind from this baseline test and keep MCP kind coverage in the round-trip test below.

Proposed test fix
 				Handle:      "read",
 				Description: "Read action",
 				Permission:  "test-server:resource1:read",
-				Kind:        providers.ActionKindTool,
 			},
 		},
 	}
@@
 	assert.Equal(s.T(), serverID, dto.ID)
 	assert.Equal(s.T(), "Test Server", dto.Name)
 	assert.Len(s.T(), dto.Resources, 1)
 	assert.Len(s.T(), dto.Resources[0].Actions, 1)
-	assert.Equal(s.T(), providers.ActionKindTool, dto.Resources[0].Actions[0].Kind)
+	assert.Empty(s.T(), dto.Resources[0].Actions[0].Kind)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/resource/declarative_resource_test.go` around lines 154 -
187, The baseline GetResourceByID test is incorrectly asserting an MCP tool kind
on a server fixture that should remain kind-free. Update the test around
exporter.GetResourceByID to remove the ActionKindTool expectation from the
default resource/action setup, since parseToResourceServer should not require or
preserve a kind for non-MCP servers. Keep Action kind coverage in the dedicated
round-trip/MCP test instead, and only assert the server/resource fields that are
valid for the default CUSTOM case.
backend/internal/resource/handler.go (1)

299-322: 🔒 Security & Privacy | 🟡 Minor

File: docs/content/guides/guides/resource-servers.mdx

🔴 Documentation Required
This PR introduces user-facing API and schema changes that are not yet reflected in the documentation. Please update the relevant files to cover the following:

  • API Reference: Add the new kind query parameter to the GET /resource-servers/{id}/actions endpoint in docs/content/apis.mdx, specifying allowed values (tool, resource).
  • Action Payloads: Document the new kind field in action create request/response schemas, including MCP-specific semantics.
  • Guides: Update docs/content/guides/guides/resource-servers.mdx to clarify that the handle field is now required when creating actions and explain related validation logic.

Missing documentation:

  • docs/content/apis.mdx: Update GET /resource-servers/{id}/actions section to include the kind parameter.
  • docs/content/guides/guides/resource-servers.mdx: Clarify the handle requirement and update the action creation example.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/resource/handler.go` around lines 299 - 322, The new `kind`
query filter in `HandleActionListAtResourceServerRequest` is not documented, and
the resource-server guide still reflects the old action creation rules. Update
`docs/content/apis.mdx` for `GET /resource-servers/{id}/actions` to list the
`kind` parameter with allowed values `tool` and `resource`, and revise
`docs/content/guides/guides/resource-servers.mdx` to state that `handle` is
required when creating actions and to update the action creation example and
validation notes accordingly.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/internal/resource/declarative_resource_test.go`:
- Around line 414-459: The MCP parser tests are using YAML fixtures that are now
invalid because they omit the required server handle, so the failures are no
longer isolated to action kind validation. Update the fixtures in
TestParseToResourceServer_MCPActionRequiresKind and
TestParseToResourceServer_MCPActionWithKindSucceeds to include a valid MCP
server handle so parseToResourceServer is exercising the intended action-kind
behavior. Keep the existing assertions around providers.ActionKindResource,
providers.ActionKindTool, and the “requires a valid kind” error so the tests
continue to validate the correct path.

In `@backend/internal/resource/declarative_resource.go`:
- Around line 288-309: The MCP validation in parseToResourceServer is missing a
required server handle check, so type MCP can still pass through with an empty
handle before nested action validation. Add an early validation in
parseToResourceServer for MCP resource servers to reject empty rs.Handle before
the action loop, using the existing resource server type checks and error style
alongside rs.Type, rs.Handle, and ProcessResourceServer so permissions are never
derived without a handle.

---

Outside diff comments:
In `@backend/internal/resource/declarative_resource_test.go`:
- Around line 154-187: The baseline GetResourceByID test is incorrectly
asserting an MCP tool kind on a server fixture that should remain kind-free.
Update the test around exporter.GetResourceByID to remove the ActionKindTool
expectation from the default resource/action setup, since parseToResourceServer
should not require or preserve a kind for non-MCP servers. Keep Action kind
coverage in the dedicated round-trip/MCP test instead, and only assert the
server/resource fields that are valid for the default CUSTOM case.

In `@backend/internal/resource/handler.go`:
- Around line 299-322: The new `kind` query filter in
`HandleActionListAtResourceServerRequest` is not documented, and the
resource-server guide still reflects the old action creation rules. Update
`docs/content/apis.mdx` for `GET /resource-servers/{id}/actions` to list the
`kind` parameter with allowed values `tool` and `resource`, and revise
`docs/content/guides/guides/resource-servers.mdx` to state that `handle` is
required when creating actions and to update the action creation example and
validation notes accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ab091ac-74e4-4adc-9cee-4a8d9e85840d

📥 Commits

Reviewing files that changed from the base of the PR and between 80e4bbc and 1c9fd23.

⛔ Files ignored due to path filters (2)
  • backend/tests/mocks/resourcemock/ResourceServiceInterface_mock.go is excluded by !**/*_mock.go
  • backend/tests/mocks/resourcemock/resourceStoreInterface_mock.go is excluded by !**/*_mock.go
📒 Files selected for processing (21)
  • api/resource.yaml
  • backend/internal/authzen/service.go
  • backend/internal/authzen/service_test.go
  • backend/internal/resource/ResourceServiceInterface_mock_test.go
  • backend/internal/resource/composite_store.go
  • backend/internal/resource/composite_store_test.go
  • backend/internal/resource/declarative_resource.go
  • backend/internal/resource/declarative_resource_test.go
  • backend/internal/resource/file_based_store.go
  • backend/internal/resource/file_based_store_test.go
  • backend/internal/resource/handler.go
  • backend/internal/resource/handler_test.go
  • backend/internal/resource/model.go
  • backend/internal/resource/resourceStoreInterface_mock_test.go
  • backend/internal/resource/service.go
  • backend/internal/resource/service_test.go
  • backend/internal/resource/store.go
  • backend/internal/resource/store_constants.go
  • backend/internal/resource/store_test.go
  • backend/pkg/thunderidengine/providers/model.go
  • backend/pkg/thunderidengine/providers/model_test.go

Comment on lines +414 to +459
func TestParseToResourceServer_MCPActionRequiresKind(t *testing.T) {
yamlData := []byte(`
id: "rs1"
name: "Test Server"
type: "MCP"
ouId: "ou1"
resources:
- name: "Users"
handle: "users"
actions:
- name: "Read"
handle: "read"
`)

dto, err := parseToResourceServer(yamlData)

assert.Error(t, err)
assert.Nil(t, dto)
assert.Contains(t, err.Error(), "read")
assert.Contains(t, err.Error(), "requires a valid kind")
}

func TestParseToResourceServer_MCPActionWithKindSucceeds(t *testing.T) {
yamlData := []byte(`
id: "rs1"
name: "Test Server"
type: "MCP"
ouId: "ou1"
resources:
- name: "Users"
handle: "users"
actions:
- name: "Read"
handle: "read"
kind: "resource"
- name: "Create"
handle: "create"
kind: "tool"
`)

dto, err := parseToResourceServer(yamlData)

assert.NoError(t, err)
assert.NotNil(t, dto)
assert.Equal(t, providers.ActionKindResource, dto.Resources[0].Actions[0].Kind)
assert.Equal(t, providers.ActionKindTool, dto.Resources[0].Actions[1].Kind)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add MCP handles to parser fixtures.

These MCP YAML fixtures omit handle; after enforcing the required MCP server handle, the “requires kind” test would fail for the wrong reason and the success test would encode an invalid MCP document.

Proposed fixture fix
 id: "rs1"
 name: "Test Server"
 type: "MCP"
+handle: "test-server"
 ouId: "ou1"
 resources:
@@
 id: "rs1"
 name: "Test Server"
 type: "MCP"
+handle: "test-server"
 ouId: "ou1"
 resources:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestParseToResourceServer_MCPActionRequiresKind(t *testing.T) {
yamlData := []byte(`
id: "rs1"
name: "Test Server"
type: "MCP"
ouId: "ou1"
resources:
- name: "Users"
handle: "users"
actions:
- name: "Read"
handle: "read"
`)
dto, err := parseToResourceServer(yamlData)
assert.Error(t, err)
assert.Nil(t, dto)
assert.Contains(t, err.Error(), "read")
assert.Contains(t, err.Error(), "requires a valid kind")
}
func TestParseToResourceServer_MCPActionWithKindSucceeds(t *testing.T) {
yamlData := []byte(`
id: "rs1"
name: "Test Server"
type: "MCP"
ouId: "ou1"
resources:
- name: "Users"
handle: "users"
actions:
- name: "Read"
handle: "read"
kind: "resource"
- name: "Create"
handle: "create"
kind: "tool"
`)
dto, err := parseToResourceServer(yamlData)
assert.NoError(t, err)
assert.NotNil(t, dto)
assert.Equal(t, providers.ActionKindResource, dto.Resources[0].Actions[0].Kind)
assert.Equal(t, providers.ActionKindTool, dto.Resources[0].Actions[1].Kind)
func TestParseToResourceServer_MCPActionRequiresKind(t *testing.T) {
yamlData := []byte(`
id: "rs1"
name: "Test Server"
type: "MCP"
handle: "test-server"
ouId: "ou1"
resources:
- name: "Users"
handle: "users"
actions:
- name: "Read"
handle: "read"
`)
dto, err := parseToResourceServer(yamlData)
assert.Error(t, err)
assert.Nil(t, dto)
assert.Contains(t, err.Error(), "read")
assert.Contains(t, err.Error(), "requires a valid kind")
}
func TestParseToResourceServer_MCPActionWithKindSucceeds(t *testing.T) {
yamlData := []byte(`
id: "rs1"
name: "Test Server"
type: "MCP"
handle: "test-server"
ouId: "ou1"
resources:
- name: "Users"
handle: "users"
actions:
- name: "Read"
handle: "read"
kind: "resource"
- name: "Create"
handle: "create"
kind: "tool"
`)
dto, err := parseToResourceServer(yamlData)
assert.NoError(t, err)
assert.NotNil(t, dto)
assert.Equal(t, providers.ActionKindResource, dto.Resources[0].Actions[0].Kind)
assert.Equal(t, providers.ActionKindTool, dto.Resources[0].Actions[1].Kind)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/resource/declarative_resource_test.go` around lines 414 -
459, The MCP parser tests are using YAML fixtures that are now invalid because
they omit the required server handle, so the failures are no longer isolated to
action kind validation. Update the fixtures in
TestParseToResourceServer_MCPActionRequiresKind and
TestParseToResourceServer_MCPActionWithKindSucceeds to include a valid MCP
server handle so parseToResourceServer is exercising the intended action-kind
behavior. Keep the existing assertions around providers.ActionKindResource,
providers.ActionKindTool, and the “requires a valid kind” error so the tests
continue to validate the correct path.

Comment on lines +288 to +309
// Enforce the action kind discriminator rules per resource server type (mirrors the REST path).
// MCP resource servers require a valid kind on each nested action; API/CUSTOM resource servers
// must not carry a kind.
for i := range rs.Resources {
for j := range rs.Resources[i].Actions {
action := rs.Resources[i].Actions[j]
if rs.Type == providers.ResourceServerTypeMCP {
if !action.Kind.IsValid() {
return nil, fmt.Errorf(
"action %q in resource server '%s' requires a valid kind (tool|resource)",
action.Handle, rs.Name,
)
}
} else if action.Kind != "" {
return nil, fmt.Errorf(
"action %q in resource server '%s' must not specify a kind",
action.Handle, rs.Name,
)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject MCP declarative servers without a handle.

parseToResourceServer now enforces MCP action kind, but still accepts type: MCP with an empty server handle; then ProcessResourceServer derives permissions without the required server handle. Add the MCP handle check before validating nested actions.

Proposed fix
 	if rs.Type == "" {
 		rs.Type = providers.ResourceServerTypeCustom
 	}
+	if rs.Type == providers.ResourceServerTypeMCP && rs.Handle == "" {
+		return nil, fmt.Errorf("resource server '%s' requires a non-empty handle for MCP type", rs.Name)
+	}
 
 	// Enforce the action kind discriminator rules per resource server type (mirrors the REST path).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Enforce the action kind discriminator rules per resource server type (mirrors the REST path).
// MCP resource servers require a valid kind on each nested action; API/CUSTOM resource servers
// must not carry a kind.
for i := range rs.Resources {
for j := range rs.Resources[i].Actions {
action := rs.Resources[i].Actions[j]
if rs.Type == providers.ResourceServerTypeMCP {
if !action.Kind.IsValid() {
return nil, fmt.Errorf(
"action %q in resource server '%s' requires a valid kind (tool|resource)",
action.Handle, rs.Name,
)
}
} else if action.Kind != "" {
return nil, fmt.Errorf(
"action %q in resource server '%s' must not specify a kind",
action.Handle, rs.Name,
)
}
}
}
if rs.Type == "" {
rs.Type = providers.ResourceServerTypeCustom
}
if rs.Type == providers.ResourceServerTypeMCP && rs.Handle == "" {
return nil, fmt.Errorf("resource server '%s' requires a non-empty handle for MCP type", rs.Name)
}
// Enforce the action kind discriminator rules per resource server type (mirrors the REST path).
// MCP resource servers require a valid kind on each nested action; API/CUSTOM resource servers
// must not carry a kind.
for i := range rs.Resources {
for j := range rs.Resources[i].Actions {
action := rs.Resources[i].Actions[j]
if rs.Type == providers.ResourceServerTypeMCP {
if !action.Kind.IsValid() {
return nil, fmt.Errorf(
"action %q in resource server '%s' requires a valid kind (tool|resource)",
action.Handle, rs.Name,
)
}
} else if action.Kind != "" {
return nil, fmt.Errorf(
"action %q in resource server '%s' must not specify a kind",
action.Handle, rs.Name,
)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/resource/declarative_resource.go` around lines 288 - 309,
The MCP validation in parseToResourceServer is missing a required server handle
check, so type MCP can still pass through with an empty handle before nested
action validation. Add an early validation in parseToResourceServer for MCP
resource servers to reject empty rs.Handle before the action loop, using the
existing resource server type checks and error style alongside rs.Type,
rs.Handle, and ProcessResourceServer so permissions are never derived without a
handle.

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.98246% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
backend/internal/resource/service.go 83.78% 4 Missing and 2 partials ⚠️
backend/internal/resource/declarative_resource.go 86.11% 4 Missing and 1 partial ⚠️
backend/internal/resource/store.go 98.24% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Model MCP tools and resources as actions distinguished by a kind
("tool"|"resource") stored in ACTION.PROPERTIES, surfaced as a flat
Action.Kind field (mirroring the RESOURCE_SERVER delimiter pattern).

- kind is optional on create for all resource server types; MCP-type
  servers default an omitted kind to "tool"; API/CUSTOM may set a kind
  but are not required to. A provided kind must be "tool" or "resource".
- kind is immutable after create (the update path does not accept it).
- MCP-gated cross-entity handle/permission collision guard, on the REST
  and declarative-import paths.
- kind on the create request/response; GET .../actions?kind= filter
  (Postgres and SQLite variants) with a matching filtered count.
- Declarative import/export preserve kind; OpenAPI updated.

Refs thunder-id#3465
@sahandilshan sahandilshan force-pushed the mcp-rs-kind-backend branch from 1c9fd23 to d814ba4 Compare June 26, 2026 09:50
@sahandilshan sahandilshan removed the breaking change The feature/ improvement will alter the existing behaviour label Jun 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/resource.yaml`:
- Around line 1168-1173: Update the example UUID values in the resource schema
so they are valid UUIDs composed only of hex characters and hyphens, since the
current examples in the booking/resource definitions use invalid characters and
conflict with the documented uuid format. Replace the affected example ids in
the resource examples block and the other referenced example sections with
schema-valid UUIDs, keeping the surrounding fields and structure unchanged.
- Around line 1238-1247: Update the invalid-kind example in the resource
validation examples so it matches a GET query-parameter failure, not a malformed
request body. In the relevant example blocks for the invalid-kind response,
change the description text to refer to an invalid query parameter or query
validation issue, and keep the error key/value structure consistent with the
surrounding resource validation examples. Use the invalid-kind example entry to
locate and mirror the same fix in both places.

In `@backend/internal/resource/declarative_resource_test.go`:
- Around line 190-253: The MCP round-trip test only covers nested resource
actions, but server-level actions are still dropped because GetResourceByID only
exports actions via GetActionList tied to each resource and ResourceServer has
no top-level Actions field. Update the declarative export/import path in
GetResourceByID and the related providers.ResourceServer model so server-scoped
MCP actions are preserved, and extend this test to assert that top-level actions
survive the export-to-YAML and parseToResourceServer import round-trip alongside
the existing resource-level action checks.

In `@backend/internal/resource/service.go`:
- Around line 1076-1079: The filtered action listing in GetActionList drops the
kind query parameter when building pagination links, so self/next/prev can point
to an unfiltered endpoint. Update the baseURL and link generation in
GetActionList, preserving the kind value alongside limit/offset, and make the
same fix in the pagination link assembly used later in the function so filtered
pages stay consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c63c508e-0b57-4dfe-b982-f036a965133e

📥 Commits

Reviewing files that changed from the base of the PR and between 1c9fd23 and d814ba4.

⛔ Files ignored due to path filters (2)
  • backend/tests/mocks/resourcemock/ResourceServiceInterface_mock.go is excluded by !**/*_mock.go
  • backend/tests/mocks/resourcemock/resourceStoreInterface_mock.go is excluded by !**/*_mock.go
📒 Files selected for processing (21)
  • api/resource.yaml
  • backend/internal/authzen/service.go
  • backend/internal/authzen/service_test.go
  • backend/internal/resource/ResourceServiceInterface_mock_test.go
  • backend/internal/resource/composite_store.go
  • backend/internal/resource/composite_store_test.go
  • backend/internal/resource/declarative_resource.go
  • backend/internal/resource/declarative_resource_test.go
  • backend/internal/resource/file_based_store.go
  • backend/internal/resource/file_based_store_test.go
  • backend/internal/resource/handler.go
  • backend/internal/resource/handler_test.go
  • backend/internal/resource/model.go
  • backend/internal/resource/resourceStoreInterface_mock_test.go
  • backend/internal/resource/service.go
  • backend/internal/resource/service_test.go
  • backend/internal/resource/store.go
  • backend/internal/resource/store_constants.go
  • backend/internal/resource/store_test.go
  • backend/pkg/thunderidengine/providers/model.go
  • backend/pkg/thunderidengine/providers/model_test.go
✅ Files skipped from review due to trivial changes (3)
  • backend/pkg/thunderidengine/providers/model_test.go
  • backend/internal/resource/ResourceServiceInterface_mock_test.go
  • backend/internal/resource/resourceStoreInterface_mock_test.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • backend/internal/resource/store_constants.go
  • backend/pkg/thunderidengine/providers/model.go
  • backend/internal/resource/file_based_store_test.go
  • backend/internal/resource/file_based_store.go
  • backend/internal/resource/composite_store.go
  • backend/internal/resource/handler.go
  • backend/internal/resource/model.go
  • backend/internal/authzen/service_test.go
  • backend/internal/authzen/service.go
  • backend/internal/resource/declarative_resource.go
  • backend/internal/resource/composite_store_test.go
  • backend/internal/resource/handler_test.go
  • backend/internal/resource/store.go
  • backend/internal/resource/store_test.go

Comment thread api/resource.yaml
Comment on lines +1168 to +1173
- id: "9c6d3g0g-7e8b-4d82-b454-4d2ccg87gh5e"
name: "Create Booking"
description: "Create a new booking"
handle: "create"
permission: "create"
- id: "ad7e4h1h-8f9c-4e93-c565-5e3ddf98hi6f"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use schema-valid UUIDs in the new examples.

The new id example values contain non-hex characters (g, h, i, j), so they do not satisfy the documented format: uuid. Some OpenAPI tooling validates examples and will flag these payloads as invalid.

Also applies to: 1188-1194, 1718-1723, 1738-1738

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/resource.yaml` around lines 1168 - 1173, Update the example UUID values
in the resource schema so they are valid UUIDs composed only of hex characters
and hyphens, since the current examples in the booking/resource definitions use
invalid characters and conflict with the documented uuid format. Replace the
affected example ids in the resource examples block and the other referenced
example sections with schema-valid UUIDs, keeping the surrounding fields and
structure unchanged.

Comment thread api/resource.yaml
Comment on lines +1238 to +1247
invalid-kind:
summary: Invalid kind parameter value
value:
code: "RES-1001"
message:
key: "error.resourceservice.invalid_request_format"
defaultValue: "Invalid request format"
description:
key: "error.resourceservice.invalid_request_format_description"
defaultValue: "The request body is malformed or contains invalid data"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the invalid-kind error examples.

These are GET query-validation failures, but the example description says the request body is malformed. That makes the contract misleading for clients diagnosing a bad kind parameter.

Also applies to: 1782-1791

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/resource.yaml` around lines 1238 - 1247, Update the invalid-kind example
in the resource validation examples so it matches a GET query-parameter failure,
not a malformed request body. In the relevant example blocks for the
invalid-kind response, change the description text to refer to an invalid query
parameter or query validation issue, and keep the error key/value structure
consistent with the surrounding resource validation examples. Use the
invalid-kind example entry to locate and mirror the same fix in both places.

Comment on lines +190 to +253
func (s *ResourceServerExporterTestSuite) TestGetResourceByID_MCPExportImportRoundTrip() {
ctx := context.Background()
serverID := "rs-mcp"

server := &providers.ResourceServer{
ID: serverID,
Name: "Booking MCP",
Handle: "booking-mcp",
Identifier: "booking-mcp",
Type: providers.ResourceServerTypeMCP,
OUID: "ou1",
Delimiter: ":",
}

resources := []providers.Resource{
{
ID: "res1",
Name: "User Management",
Handle: "user-mgmt",
Parent: nil,
Permission: "booking-mcp:user-mgmt",
},
}

actions := &ActionList{
TotalResults: 1,
Actions: []providers.Action{
{
ID: "act1",
Name: "Create User",
Handle: "create_user",
Permission: "booking-mcp:user-mgmt:create_user",
Kind: providers.ActionKindTool,
},
},
}

resourceID := "res1"
s.mockService.EXPECT().GetResourceServer(ctx, serverID).Return(server, nil)
s.mockService.EXPECT().GetAllResourceList(ctx, serverID).Return(resources, nil)
s.mockService.EXPECT().
GetActionList(ctx, serverID, &resourceID, providers.ActionKind(""), serverconst.MaxPageSize, 0).
Return(actions, nil)

result, _, err := s.exporter.GetResourceByID(ctx, serverID)
assert.Nil(s.T(), err)

dto, ok := result.(*providers.ResourceServer)
assert.True(s.T(), ok)

// Marshal the exported DTO to YAML, then re-import it. This guards the export->import
// lossless guarantee: type and handle must survive so the kind-vs-type import validation
// accepts the nested action carrying a kind.
yamlBytes, marshalErr := yaml.Marshal(dto)
assert.NoError(s.T(), marshalErr)

imported, parseErr := parseToResourceServer(yamlBytes)
s.Require().NoError(parseErr)
s.Require().NotNil(imported)
assert.Equal(s.T(), providers.ResourceServerTypeMCP, imported.Type)
assert.Equal(s.T(), "booking-mcp", imported.Handle)
assert.Len(s.T(), imported.Resources, 1)
assert.Len(s.T(), imported.Resources[0].Actions, 1)
assert.Equal(s.T(), providers.ActionKindTool, imported.Resources[0].Actions[0].Kind)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Server-level MCP actions are still unrepresented in declarative export/import.

This round-trip only covers actions under a resource. In the supplied GetResourceByID implementation, export walks GetAllResourceList and only calls GetActionList(..., &res.ID, ...), while providers.ResourceServer has no top-level Actions field. That means the new “resource is optional grouping namespace” case can’t round-trip: actions attached directly to the resource server will be dropped on export/import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/resource/declarative_resource_test.go` around lines 190 -
253, The MCP round-trip test only covers nested resource actions, but
server-level actions are still dropped because GetResourceByID only exports
actions via GetActionList tied to each resource and ResourceServer has no
top-level Actions field. Update the declarative export/import path in
GetResourceByID and the related providers.ResourceServer model so server-scoped
MCP actions are preserved, and extend this test to assert that top-level actions
survive the export-to-YAML and parseToResourceServer import round-trip alongside
the existing resource-level action checks.

Comment on lines +1076 to +1079
// If kind is non-empty, only actions of that kind are returned.
func (rs *resourceService) GetActionList(
ctx context.Context,
resourceServerID string, resourceID *string, limit, offset int,
resourceServerID string, resourceID *string, kind providers.ActionKind, limit, offset int,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve kind in pagination links.

Line 1079 adds filtered listing, but the baseURL built below never carries that filter. A GET .../actions?kind=tool page will return filtered data while self/next/prev links point at the unfiltered endpoint, so following pagination links changes the result set.

Also applies to: 1127-1141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/resource/service.go` around lines 1076 - 1079, The filtered
action listing in GetActionList drops the kind query parameter when building
pagination links, so self/next/prev can point to an unfiltered endpoint. Update
the baseURL and link generation in GetActionList, preserving the kind value
alongside limit/offset, and make the same fix in the pagination link assembly
used later in the function so filtered pages stay consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant